/*
 * vim: ts=4 sw=4 fdm=marker noet tw=78 ai
 * %%LICENSE%%
 */
#include <sys/cdefs.h>
#ifdef __FBSDID
__FBSDID("$Coar$");
#endif

#include <sys/limits.h>

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <getopt.h>
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <assert.h>

#include <err.h>
#include <sysexits.h>

#include <ldap.h>
#include <ldif.h>

#include "extern.h"
#include "connect.h"
#include "search.h"
#include "classes.h"
#include "fetch.h"
#include "attributes.h"
#include "attrdfn.h"
#include "hash.h"
#include "lde_ldif.h"
#include "modify.h"

/*
 * Basic ldap* paramaters and our own:
 * - -e: editmode
 * - -C: compact format
 */
static const char *opts = ":b:CD:ef:H:h:Op:R:s:U:vWw:X:xY:y:Z";

/* Forward decls */
static void usage(void);

int
main(int argc, char *argv[])
{
	int ch, res, ex = EX_OK;
	const char *errstr = NULL;
	char *ldapmsg = NULL, *tmpdir;
	runtime_t *ctx;
	LDAPMessage *resp = NULL;
	LDAPMessage *entry = NULL;

	ctx = malloc(sizeof(runtime_t));
	if( !ctx )
		err(EX_OSERR, "Can't allocate runtime context");
	memset(ctx, 0, sizeof(runtime_t));
	ctx->opt_scope = LDAP_SCOPE_SUB; // default scope

	if( getenv("LDAP_USE_TLS") )
		ctx->opt_tls = 1;
	if( getenv("LDAP_REQUIRE_TLS") )
		ctx->opt_tls = 2;
	if( (tmpdir = getenv("TMPDIR")) == NULL )
		tmpdir = strdup("/tmp");

	ctx->env_editor = getenv("EDITOR");
	ctx->me = getprogname();
	if( STR_EQ(ctx->me, "ldapedit") )
		ctx->opt_editmode = 1;

	/* {{{ Process options */
	while( (ch = getopt(argc, argv, opts)) != -1 )
	{
		switch(ch)
		{
			case 'b' :
				ctx->opt_base = strdup(optarg);
				break;
			case 'C' :
				ctx->opt_compact = 1;
				break;
			case 'D' :
				ctx->opt_conn_dn = strdup(optarg);
				break;
			case 'e' :
				ctx->opt_editmode = 1;
				break;
			case 'f' :
				ctx->opt_file = fopen(optarg, "w");
				if( !ctx->opt_file )
					err(EX_CANTCREAT, "Can't write to %s", optarg);
				STRDUP(ctx->fname, optarg);
				break;
			case 'H' :
				ctx->opt_conn_uri = strdup(optarg);
				if( ctx->opt_conn_host || ctx->opt_conn_port )
					warnx("Host and port argument are ignored.");
				break;
			case 'h' :
				ctx->opt_conn_host = strdup(optarg);
				if( ctx->opt_conn_uri )
					warnx("Host is ignored as URI is specified.");
				break;
			case 'O' :
				ctx->opt_conn_sasl_props = strdup(optarg);
				break;
			case 'p' :
				ctx->opt_conn_port = (uint16_t)strtonum(optarg, 1, USHRT_MAX,
						&errstr);
				if( errstr )
					errx(EX_USAGE, "Invalid port %s: %s", optarg, errstr);
				if( ctx->opt_conn_uri )
					warnx("URI set, port argument ignored");
				break;
			case 'R' :
				ctx->opt_conn_sasl_realm = strdup(optarg);
				break;
			case 's' :
				if( STR_EQ(optarg, "base") )
					ctx->opt_scope = LDAP_SCOPE_BASE;
				else if( STR_EQ(optarg, "sub") || STR_EQ(optarg, "subtree") )
					ctx->opt_scope = LDAP_SCOPE_SUB;
				else if( STR_EQ(optarg, "subordinate") )
					ctx->opt_scope = LDAP_SCOPE_SUBORDINATE;
				else if( STR_EQ(optarg, "one") )
					ctx->opt_scope = LDAP_SCOPE_ONELEVEL;
				else
					errx(EX_USAGE, "Invalid scope: %s", optarg);
				break;
			case 'U' :
				ctx->opt_conn_sasl_authcid = strdup(optarg);
				break;
			case 'v' :
				ctx->opt_verbose++;
				break;
			case 'W' :
				ctx->opt_conn_pwprompt = 1;
				break;
			case 'w' :
				ctx->pw.bv_val = strdup(optarg);
				ctx->pw.bv_len = strlen(optarg);
				break;
			case 'X' :
				ctx->opt_conn_sasl_authzid = strdup(optarg);
				break;
			case 'x' :
				ctx->opt_conn_sasl_mech = LDAP_SASL_SIMPLE;
				break;
			case 'Y' :
				ctx->opt_conn_sasl_mech = strdup(optarg);
				break;
			case 'y' :
				if( STR_EQ(optarg, "-") )
					ctx->opt_conn_pwfile = stdin;
				else
				{
					ctx->opt_conn_pwfile = fopen(optarg, "r");
					if( ! ctx->opt_conn_pwfile )
						err(EX_NOINPUT, "Can't read %s", optarg);
				}
				break;
			case 'Z' :
				ctx->opt_tls++;
				break;
			default :
				warn("Invalid option: -%c", ch);
				usage();
				/* NOT REACHED */
		}
	}
	/* }}} */

	/*
	 * {{{ Post process flags:
	 * - Check which authentication method to use
	 *   - If SASL settings are present, make sure mechanism is provided.
	 * - set opt_file to stdout if still NULL
	 * - Fill editor with "vi" if none set
	 * - Make sure we're not "editing" stdout
	 */
	if( ctx->opt_conn_sasl_authcid || ctx->opt_conn_sasl_authzid ||
			ctx->opt_conn_sasl_props || ctx->opt_conn_sasl_realm ||
			ctx->opt_conn_sasl_mech
	  )
	{
		if( ! ctx->opt_conn_sasl_mech )
		{
			warnx("Forcing SASL mechanism to PLAIN");
			ctx->opt_conn_sasl_mech = strdup("PLAIN");
		}
	}
	else
	{
		ctx->opt_conn_sasl_mech = LDAP_SASL_SIMPLE;
	}
	if( ctx->opt_file == stdout && ctx->opt_editmode )
		err(EX_USAGE, "Can't edit stdout.");

	if( ctx->opt_file == NULL )
	{
		if( ctx->opt_editmode )
		{
			char *tpl = NULL;
			int fd;

			if( asprintf(&tpl, "%s/ldapformXXXXXX.ldif", tmpdir) == -1 )
				err(EX_OSERR, "Can't create template for temporary file");
			/*
			 * We use mkstemps to preserve the extension. Some editors have
			 * plugins/highlighting for ldif and use extension to enable it.
			 * This saves us writing "mode lines" for editors.
			 */
			if( (fd = mkstemps(tpl, 5)) < 0 )
				err(EX_CANTCREAT, "Can't create temporary file");
			STRDUP(ctx->fname, tpl);

			if( (ctx->opt_file = fdopen(fd, "w+")) == NULL )
				err(EX_NOINPUT, "Can't fdopen() temporary file");
			free(tmpdir);
			free(tpl);
		}
		else
			ctx->opt_file = stdout;
	}
	if( ctx->opt_editmode )
	{
		if( ctx->env_editor == NULL || ctx->env_editor[0] == '\0' )
		{
			if( (ctx->env_editor = strdup("vi")) == NULL )
				err(EX_OSERR, "Can't copy editor");
		}
	}
	/* }}} */

	argc -= optind;
	argv += optind;

	if( argc == 0 )
	{
		warnx("No entry given");
		usage();
	}

	ctx->arg_entry = argv[0];

	res = lde_connect(ctx);
	if( res != LDAP_SUCCESS )
	{
		ldapmsg = ldap_err2string(res);
		err(EX_UNAVAILABLE, "Failed to connect: %s", ldapmsg);
	}

	LOG(ctx, 1, "Bind successful");
	assert(ctx->ld != NULL);
	/*
	 * Overall process:
	 * - Fetch the object class definitions from the LDAP server;
	 * - These are put in a linked list ctx->ocl;
	 * - Fetch the attribute definitions from the LDAP server;
	 * - These are put in a linked list ctx->adl;
	 * - Fetch the requested entry from the LDAP server;
	 * - Process the entry:
	 *   - For each object class, look up the attributes of the class;
	 *   - Add new attributes to the global attribute list;
	 *   - For other attributes, look them up in the global attribute list,
	 *     add them if they're not present, set their value otherwise;
	 * - Print out the attribute list in LDIF format.
	 */
	LOG(ctx, 1, "Fetching object class definitions");
	res = lde_fetch_classes(ctx);
	if( res != LDAP_SUCCESS )
	{
		warnx("Failed to fetch server supported class definitions: %s",
				ldap_err2string(res));
		ex = EX_NOPERM;
		goto close;
	}
	else
		LOG(ctx, 1, "Added %zu objectclasses", ctx->ocl->cnt);

	LOG(ctx, 1, "Fetching attribute definitions");
	res = lde_fetch_attr_definitions(ctx);
	if( res != LDAP_SUCCESS )
	{
		warn("Failed to fetch server supported attribute definitions: %s",
				ldap_err2string(res));
		ex = EX_NOPERM;
		goto close;
	}
	else
		LOG(ctx, 1, "Added %zu attribute definitions", ctx->adl->cnt);

	res = lde_search_entry(ctx, &resp);
	if( res != LDAP_SUCCESS )
	{
		warnx("Failed to fetch requested entry %s: %s", ctx->arg_entry,
				ldap_err2string(res));
		ex = EX_PROTOCOL;
		goto close;
	}
	res = ldap_count_entries(ctx->ld, resp);
	if( res == 0 )
	{
		warnx("No matches found for %s", ctx->arg_entry);
		ex = EX_NOINPUT;
		goto close;
	}
	LOG(ctx, 1, "Processing %i matches for filter `%s'", res, ctx->arg_entry);
	for(
			entry = ldap_first_entry(ctx->ld, resp);
			entry != NULL;
			entry = ldap_next_entry(ctx->ld, entry)
	   )
	{
		char *dn; // Distinguished Name
		char *att_name; // Attribute name
		BerElement *tr = NULL; // Tracker for the attr iterator
		attr_entry_t *cur; // Current attribute entry in attribute list

		dn = ldap_get_dn(ctx->ld, entry);
		lde_write_ldif(ctx, LDIF_PUT_COMMENT, NULL, dn, strlen(dn));
		lde_write_ldif(ctx, LDIF_PUT_VALUE, "dn", dn, strlen(dn));
		if( ! ctx->opt_compact )
			lde_write_ldif(ctx, LDIF_PUT_VALUE, "changetype", NULL, 0);
		// For each entry, initialize a new attribute list
		lde_attribute_list_init(&(ctx->atl));
		for(
				att_name = ldap_first_attribute(ctx->ld, entry, &tr);
				att_name != NULL;
				att_name = ldap_next_attribute(ctx->ld, entry, tr)
		   )
		{
			struct berval **vals; // Attribute values array
			size_t i; // values array iterator

			vals = ldap_get_values_len(ctx->ld, entry, att_name);
			if( vals == NULL )
				continue;
			/*
			 * We handle objectclasses differently:
			 * - Once we find one, we fetch the object class definition from
			 *   the object class list created earlier and add it's attributes
			 *   to the attribute list.
			 * - We immediately print it's ldif representation.
			 * - We do not add the objectclass attribute itself to the
			 *   attribute list
			 */
			if( ATTR_IS_OC(att_name) )
			{
				if( vals == NULL )
				{
					warn("Object class with no values.");
					ex = EX_PROTOCOL;
					goto close;
				}
				for( i=0; vals[i] != NULL; i++ )
				{
					LDE_WRITE_DELETE_OC(ctx);
					lde_write_ldif(ctx, LDIF_PUT_VALUE, "objectClass",
							vals[i]->bv_val, vals[i]->bv_len);
					LDE_WRITE_CONTINUE(ctx);
					lde_fetch_class_attributes(ctx, ctx->atl,
							vals[i]->bv_val);
				}
			}
			/*
			 * Process an attribute:
			 * - iterate values
			 * - pass name and value to appropriate function
			 */
			else
			{
				vals = ldap_get_values_len(ctx->ld, entry, att_name);
				for( i=0; vals[i] != NULL; i++ )
				{
					res = lde_add_attribute_value(ctx, ctx->atl, att_name,
							vals[i]);
					if( res != LDAP_SUCCESS )
					{
						warn("Failed to add attribute %s: %s", att_name,
								ldap_err2string(res));
					}
				}
			}
			ldap_value_free_len(vals);
			LOG(ctx, 2, "Attribute count is %zu", ctx->atl->cnt);
		}// attribute loop
		/*
		 * Print out remaining attributes.
		 * The side effect of above approach is that object classes are
		 * printed first and all in one shot. I don't consider this a bad
		 * thing.
		 */
		STAILQ_FOREACH(cur, LDE_LIST_HEAD(ctx->atl), next)
		{
			char *descr;
			ber_len_t descr_len;

			descr = lde_get_attr_descr(ctx, cur->name, &descr_len);
			LDE_WRITE_ADD_DELETE(ctx, cur->name, cur->name_len);
			lde_write_ldif(ctx, LDIF_PUT_COMMENT, NULL, descr, descr_len);
			lde_write_ldif(ctx, LDIF_PUT_VALUE, cur->name, cur->value,
					cur->value_len);
			LDE_WRITE_CONTINUE(ctx);
		}
	}// entry loop
	if( ctx->opt_editmode )
	{
		char *cmd = NULL, oldhash[41], *fbuf, *dn;
		const char *errmsg;
		LDAPMod **mods;
		bool is_add = !!ctx->opt_compact;
		size_t nmods;

		/*
		 * XXX: we should close connection here and reopen after editor is
		 * done. This will invalidate the ld and a new one will be needed. It
		 * is however annoying to re-enter authentication information when
		 * using a password prompt. As such, we need to cache it, which also
		 * means that the password will be visible in the process' address
		 * space during editing.
		 *
		 * The alternative is to use a keep-alive, which means the not so
		 * portable realm of IPC/threading and event constructs. Also the API
		 * docs are not clear about a keep-alive operation being available,
		 * whether implemented automatically or by consumers of the API.
		 * Will need to reed the RFCs.
		 */
		// Invoke editor and compare hashes of before and after
		fclose(ctx->opt_file);
		if( asprintf(&cmd, "%s %s", ctx->env_editor, ctx->fname) < 0 )
			err(EX_OSERR, "Can't create command string");
		lde_hash_file(ctx);
		if( (res = system(cmd)) != 0 )
			errx(EX_UNAVAILABLE, "Editor failed with code %i", res);
		(void)strlcpy(oldhash, ctx->fhash, sizeof(oldhash));
		lde_hash_file(ctx);
		LOG(ctx, 1, "Hash before edit: %40s", oldhash);
		LOG(ctx, 1, "Hash after  edit: %40s", ctx->fhash);
		while( (nmods = lde_parse_mods(ctx, &fbuf, &dn, &mods, is_add,
						&errmsg)) != 0 )
		{
			res = ldap_modify_ext_s(ctx->ld, dn, mods, NULL, NULL);
		}
		if( errmsg )
		{
			warnx("Failed to parse: %s", errmsg);
			ex = EX_NOINPUT;
			goto close;
		}
		if( STR_EQ(ctx->fhash, oldhash) )
		{
			warnx("File unchanged, aborting");
			ex = EX_NOINPUT;
			goto close;
		}
	}
close:
	LOG(ctx, 1, "Closing connection");
	if( ctx->opt_file ) fclose(ctx->opt_file);
	res = ldap_unbind_ext_s(ctx->ld, NULL, NULL);
	return (ex);
}

static void
usage(void)
{
	fprintf(stderr, "Usage: %s [options] dn\n", getprogname());
	fprintf(stderr, "       Create LDIF ouptut for the entry with dn\n");
	fprintf(stderr, "       printing all attributes, inluding empty ones\n");
	exit(EX_USAGE);
}
